import { SandBox, SandBoxType } from '../interfaces';
import { nextTick, uniq } from '../utils';
import { attachDocProxySymbol, getTargetValue } from './common';
import { clearSystemJsProps, interceptSystemJsProps } from './noise/systemjs';
const rawObjectDefineProperty = Object.defineProperty;
const unscopables = {
  undefined: true,
  Array: true,
  Object: true,
  String: true,
  Boolean: true,
  Math: true,
  eval: true,
  Number: true,
  Symbol: true,
  parseFloat: true,
  Float32Array: true
};

function createFakeWindow(global) {
  const propertiesWithGetter = new Map();
  const fakeWindow = {};
  Object.getOwnPropertyNames(global).filter(p => {
    const descriptor = Object.getOwnPropertyDescriptor(global, p);
    return !descriptor?.configurable;
  }).forEach(p => {
    const descriptor = Object.getOwnPropertyDescriptor(global, p);

    if (descriptor) {
      const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');

      if (p === 'top' || p === 'parent' || p === 'self' || p === 'window' || process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop')) {
        descriptor.configurable = true;

        if (!hasGetter) {
          descriptor.writable = true;
        }
      }

      if (hasGetter) propertiesWithGetter.set(p, true);
      rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
    }
  });
  return {
    fakeWindow,
    propertiesWithGetter
  };
}

let activeSandboxCount = 0;
export default class ProxySandbox {
  updatedValueSet = new Set();
  sandboxRunning = true;

  active() {
    if (!this.sandboxRunning) activeSandboxCount++;
    this.sandboxRunning = true;
  }

  inactive() {
    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [...this.updatedValueSet.keys()]);
    }

    clearSystemJsProps(this.proxy, --activeSandboxCount === 0);
    this.sandboxRunning = false;
  }

  constructor(name) {
    this.name = name;
    this.type = SandBoxType.Proxy;
    const {
      updatedValueSet
    } = this;
    const self = this;
    const rawWindow = window;
    const {
      fakeWindow,
      propertiesWithGetter
    } = createFakeWindow(rawWindow);
    const descriptorTargetMap = new Map();

    const hasOwnProperty = key => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);

    const proxy = new Proxy(fakeWindow, {
      set(target, p, value) {
        if (self.sandboxRunning) {
          target[p] = value;
          updatedValueSet.add(p);
          interceptSystemJsProps(p, value);
          return true;
        }

        if (process.env.NODE_ENV === 'development') {
          console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
        }

        return true;
      },

      get(target, p) {
        if (p === Symbol.unscopables) return unscopables;

        if (p === 'window' || p === 'self') {
          return proxy;
        }

        if (p === 'top' || p === 'parent' || process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop')) {
          if (rawWindow === rawWindow.parent) {
            return proxy;
          }

          return rawWindow[p];
        }

        if (p === 'hasOwnProperty') {
          return hasOwnProperty;
        }

        if (p === 'document') {
          document[attachDocProxySymbol] = proxy;
          nextTick(() => delete document[attachDocProxySymbol]);
          return document;
        }

        const value = propertiesWithGetter.has(p) ? rawWindow[p] : target[p] || rawWindow[p];
        return getTargetValue(rawWindow, value);
      },

      has(target, p) {
        return p in unscopables || p in target || p in rawWindow;
      },

      getOwnPropertyDescriptor(target, p) {
        if (target.hasOwnProperty(p)) {
          const descriptor = Object.getOwnPropertyDescriptor(target, p);
          descriptorTargetMap.set(p, 'target');
          return descriptor;
        }

        if (rawWindow.hasOwnProperty(p)) {
          const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
          descriptorTargetMap.set(p, 'rawWindow');

          if (descriptor && !descriptor.configurable) {
            descriptor.configurable = true;
          }

          return descriptor;
        }

        return undefined;
      },

      ownKeys(target) {
        return uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
      },

      defineProperty(target, p, attributes) {
        const from = descriptorTargetMap.get(p);

        switch (from) {
          case 'rawWindow':
            return Reflect.defineProperty(rawWindow, p, attributes);

          default:
            return Reflect.defineProperty(target, p, attributes);
        }
      },

      deleteProperty(target, p) {
        if (target.hasOwnProperty(p)) {
          delete target[p];
          updatedValueSet.delete(p);
          return true;
        }

        return true;
      }

    });
    this.proxy = proxy;
  }

}